package com.xiaoshuidi.cloud.framework.pay.core.client.impl.tonglian;

import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xiaoshuidi.cloud.framework.common.util.json.JsonUtils;
import com.xiaoshuidi.cloud.framework.pay.core.client.PayCommonResult;
import com.xiaoshuidi.cloud.framework.pay.core.client.dto.*;
import com.xiaoshuidi.cloud.framework.pay.core.client.impl.AbstractPayClient;
import com.xiaoshuidi.cloud.framework.pay.core.client.impl.tongshangyun.dto.EnterpriseAccountEstablishReqDTO;
import com.xiaoshuidi.cloud.framework.pay.core.client.impl.tongshangyun.dto.EnterpriseAccountEstablishRespDTO;
import com.xiaoshuidi.cloud.framework.pay.core.client.impl.tongshangyun.dto.MemberBindReqDTO;
import com.xiaoshuidi.cloud.framework.pay.core.client.impl.tongshangyun.dto.MemberBindRespDTO;
import com.xiaoshuidi.cloud.framework.pay.core.client.impl.tongshangyun.dto.MemberCreateReqDTO;
import com.xiaoshuidi.cloud.framework.pay.core.client.impl.tongshangyun.dto.MemberCreateRespDTO;
import com.xiaoshuidi.cloud.framework.pay.core.enums.PayChannelEnum;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.jcajce.spec.SM2ParameterSpec;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

@Slf4j
public class TonglianAlipayMiniAppPayClient extends AbstractPayClient<TonglianClientConfig> {
    private PrivateKey sm2PrivateKey;
    private PublicKey sm2PublicKey;

    @Resource
    @Qualifier("okHttpClient")
    private OkHttpClient client;

    public TonglianAlipayMiniAppPayClient(Long channelId, TonglianClientConfig config) {
        super(channelId, PayChannelEnum.TONGLIAN_ALIPAY_CASHIER.getCode(), config, new TonglianCodeMapping());
    }

    @Override
    public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
        log.info("通联支付通知: {}", JsonUtils.toJsonString(data));
        // TODO 验签
        // 判断交易状态
        String trxstatus = data.getParams().get("trxstatus");
        if (org.apache.commons.lang3.StringUtils.equals(trxstatus,"0000")){
            // 交易成功
//            //todo 查询交易结果
//            String cusorderid = data.getParams().get("cusorderid");
//            PayQueryUnifiedReqDTO queryUnifiedReqDTO = new PayQueryUnifiedReqDTO();
//            queryUnifiedReqDTO.setMerchantOrderId(cusorderid);
//            String trxid = data.getParams().get("trxid");
//            queryUnifiedReqDTO.setTransactionSerNo(trxid);
//            PayCommonResult<?> payCommonResult = unifiedQuery(queryUnifiedReqDTO);
//            JsonNode resultData = (JsonNode)payCommonResult.getData();
//            resultData.get("retcode").asText();
            String paytime = data.getParams().get("paytime");
            LocalDateTime successTime = null;
            if (org.apache.commons.lang3.StringUtils.isNotBlank(paytime)){
                try {
                    DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
                    successTime = LocalDateTime.parse(paytime, df);
                } catch (Exception e) {
                    log.error("通联支付回调，支付成功时间转换异常：",e);
                }
            }
            String trxamt = data.getParams().get("trxamt");
            Long payAmount = null;
            if (org.apache.commons.lang3.StringUtils.isNotBlank(trxamt)){
                try {
                    payAmount = Long.parseLong(trxamt);
                } catch (NumberFormatException e) {
                    log.error("通联支付回调，支付金额转换异常：",e);
                }
            }

            return PayOrderNotifyRespDTO.builder().build()
                    .setMerchantOrderId(data.getParams().get("cusorderid"))
                    .setChannelOrderNo(data.getParams().get("trxid"))
                    .setChannelUserId(data.getParams().get("acct"))
                    .setSuccessTime(successTime)
                    .setPayAmount(payAmount)
                    .setData(JSONObject.toJSONString(data))
                    .setTradeStatus("TRADE_SUCCESS")
                    .setNotifyStatus("success")
                    .setSuccess(true);
        }else {
            throw new RuntimeException("通联支付失败");
        }
    }


    @Override
    public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
        return null;
    }

    @Override
    public PayCommonResult<?> unifiedQuery(PayQueryUnifiedReqDTO reqDTO) throws Exception {
        TreeMap<String, String> params = commonParams();
        params.put("version", "11");
        params.put("reqsn", reqDTO.getMerchantOrderId());
        params.put("trxid", reqDTO.getTransactionSerNo());
        params.put("randomstr", getValidatecode(8));
        params.put("signtype","SM2");
        sign(params,null,null);
        return call("/query",params, respBody -> new PayQueryUnifiedRespDTO());
//        throw new UnsupportedOperationException("需要实现");
    }

    @Override
    protected void doInit() {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("EC");
            sm2PrivateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(Base64.decodeBase64(super.config.getSm2PrivateKey())));
            sm2PublicKey = keyFactory.generatePublic(new X509EncodedKeySpec(Base64.decodeBase64(super.config.getSm2PublicKey())));
        } catch (NoSuchAlgorithmException ignore) {
        } catch (InvalidKeySpecException e) {
            log.error("通联SM2密钥解析错误,异常信息：{}",e);
            throw new RuntimeException("通联SM2密钥解析错误", e);
        }
    }

    @Override
    protected PayCommonResult<PayOrderUnifiedRespDTO<TreeMap<String, String>>> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Throwable {
        TreeMap<String, String> params;
        String cusPrivateKey = null;
        String appid = null;
        if (CollectionUtils.isEmpty(reqDTO.getPayChannelParam())){
            params = commonParams();
        }else {
            params = new TreeMap<>();
            Map<String, Object> payChannelParam = reqDTO.getPayChannelParam();
            if (Objects.isNull(payChannelParam.get("cusId")) || Objects.nonNull(payChannelParam.get("cusId"))){
                throw new RuntimeException("支付通道参数异常");
            }
            cusPrivateKey = payChannelParam.get("cusPrivateKey").toString();
            appid = payChannelParam.get("appId").toString();
            params.put("cusid", payChannelParam.get("cusId").toString());
            params.put("appid", appid);
        }
        params.put("version", "12");
        params.put("trxamt", reqDTO.getAmount().toString());
        params.put("reqsn", reqDTO.getMerchantOrderId());
        params.put("notify_url", reqDTO.getNotifyUrl());
        params.put("body", reqDTO.getSubject());
        params.put("remark", "");
        params.put("validtime", String.valueOf(ChronoUnit.MINUTES.between(LocalDateTime.now(), reqDTO.getExpireTime())));
//        params.put("limit_pay","no_credit");
        params.put("randomstr", getValidatecode(8));
        params.put("paytype","A02");
        params.put("signtype","SM2");
        sign(params,cusPrivateKey,appid);
        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writeValueAsString(params);
        String query = URLEncoder.encode("payinfo=" + URLEncoder.encode(json, "UTF-8"), "UTF-8");
        String url =
                "alipays://platformapi/startapp?appId=2021001104615521&page=pages/orderDetail/orderDetail&thirdPartSchema="
                        +  URLEncoder.encode("chengyu", "UTF-8")
                        + "&query=" + query;
        params.put("url", url);
        log.info("通联支付请求参数：{}", params);
        return PayCommonResult.build("success", "",
                new PayOrderUnifiedRespDTO<TreeMap<String, String>>()
                        .setRawResponse(params)
                        .setMerchantOrderId(reqDTO.getMerchantOrderId()), codeMapping);
    }

    @Override
    protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
        TreeMap<String, String> params = commonParams();
        params.put("trxamt", reqDTO.getAmount().toString());
        params.put("reqsn", reqDTO.getMerchantRefundId());
        params.put("oldreqsn", reqDTO.getPayTradeNo());
        sign(params,null,null);
        return call("/refund",params, respBody ->
                PayRefundUnifiedRespDTO.builder().channelRefundId(respBody.get("trxid").asText()).build()
        );
    }

    @Override
    public PayCommonResult<?> doUnifiedClose(PayCloseUnifiedReqDTO reqDTO) throws Throwable {
        TreeMap<String, String> params = commonParams();
        params.put("oldreqsn", reqDTO.getMerchantOrderId());
        sign(params,null,null);
        return call("/cancel",params, respBody -> null);
    }

    private <T> PayCommonResult<T> call(String url,TreeMap<String, String> params, Function<JsonNode, T> respConsumer) throws Exception {
        Request req = new Request.Builder()
                .url("https://vsp.allinpay.com/apiweb/tranx" + url)
                .post(RequestBody.create(params.entrySet()
                        .stream().map(e -> String.format("%s=%s", e.getKey(), e.getValue()))
                        .collect(Collectors.joining("&")), MediaType.get("application/x-www-form-urlencoded")))
                .build();

        Response resp = client.newCall(req).execute();
        JsonNode respBody = JsonUtils.parseTree(resp.body().bytes());
        String retcode = respBody.get("retcode").asText();
        if (Objects.equals(retcode, "SUCCESS")) {
            return PayCommonResult.build(respBody.get("retcode").asText(), respBody.get("retmsg").asText(),
                    respConsumer.apply(respBody), codeMapping);
        }
        return PayCommonResult.build(respBody.get("retcode").asText(), respBody.get("retmsg").asText(), null, codeMapping);
    }

    private TreeMap<String, String> commonParams() {
        TreeMap<String, String> params = new TreeMap<>();
        params.put("cusid", super.config.getCusid());
        params.put("appid", super.config.getAppid());
        return params;
    }

    private void sign(TreeMap<String, String> params, String privateKey, String appid) throws Exception {
        String signStr = params.entrySet().stream()
                .filter(e -> StringUtils.hasText(e.getValue()))
                .map(e -> e.getKey() + "=" + e.getValue())
                .collect(Collectors.joining("&"));
        byte[] signBytes;
        try {
            if (org.apache.commons.lang3.StringUtils.isNotBlank(privateKey) && org.apache.commons.lang3.StringUtils.isNotBlank(appid)){
                KeyFactory keyFactory = KeyFactory.getInstance("EC");
                PrivateKey privateKeyBean = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));
                signBytes = signSM3SM2(privateKeyBean, appid, signStr.getBytes(StandardCharsets.UTF_8));
            }else {
                signBytes = signSM3SM2(sm2PrivateKey, config.getAppid(), signStr.getBytes(StandardCharsets.UTF_8));
            }
        } catch (Exception e) {
            log.error("通联SM2密钥加密错误,异常信息：{}",e);
            throw new RuntimeException("支付通道参数异常");
        }
        params.put("sign", Base64.encodeBase64String(signBytes));
    }


    /**
     * 算法常量:SM3withSM2
     */
    public final String ALGORITHM_SM3SM2_BCPROV = "SM3withSM2";
    private final int SM3withSM2_RS_LEN = 32;

    public byte[] signSM3SM2(final PrivateKey privateKey, String certid, final byte[] data) throws Exception {
        SM2ParameterSpec parameterSpec = new SM2ParameterSpec(certid.getBytes());
        Signature signer = Signature.getInstance(ALGORITHM_SM3SM2_BCPROV, "BC");
        signer.setParameter(parameterSpec);
        signer.initSign(privateKey, new SecureRandom());
        signer.update(data);
        return byteAsn12BytePlain(signer.sign());
    }

    /**
     * 将ASN1字节数组转换为普通字节数组 适用于SM3withSM2签名时签名结果转换
     */
    private byte[] byteAsn12BytePlain(byte[] dataAsn1) {
        ASN1Sequence seq = ASN1Sequence.getInstance(dataAsn1);
        byte[] r = bigIntToFixexLengthBytes(ASN1Integer.getInstance(seq.getObjectAt(0)).getValue());
        byte[] s = bigIntToFixexLengthBytes(ASN1Integer.getInstance(seq.getObjectAt(1)).getValue());
        byte[] result = new byte[SM3withSM2_RS_LEN * 2];
        System.arraycopy(r, 0, result, 0, r.length);
        System.arraycopy(s, 0, result, SM3withSM2_RS_LEN, s.length);
        return result;
    }

    private byte[] bigIntToFixexLengthBytes(BigInteger rOrS) {
        byte[] rs = rOrS.toByteArray();
        if (rs.length == SM3withSM2_RS_LEN) return rs;
        else if (rs.length == SM3withSM2_RS_LEN + 1 && rs[0] == 0)
            return Arrays.copyOfRange(rs, 1, SM3withSM2_RS_LEN + 1);
        else if (rs.length < SM3withSM2_RS_LEN) {
            byte[] result = new byte[SM3withSM2_RS_LEN];
            Arrays.fill(result, (byte) 0);
            System.arraycopy(rs, 0, result, SM3withSM2_RS_LEN - rs.length, rs.length);
            return result;
        } else {
            throw new RuntimeException("err rs: " + Hex.toHexString(rs));
        }
    }

    /**
     * 生成随机码
     *
     * @param n
     * @return
     */
    public static String getValidatecode(int n) {
        Random random = new Random();
        String sRand = "";
        n = n == 0 ? 4 : n;// default 4
        for (int i = 0; i < n; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
        }
        return sRand;
    }
}
